/*
 * JBoss, Home of Professional Open Source
 * Copyright XXXX, Red Hat Middleware LLC, and individual contributors as indicated
 * by the @authors tag. All rights reserved.
 * See the copyright.txt in the distribution for a full listing
 * of individual contributors.
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License, v. 2.0.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License,
 * v. 2.0 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */
package org.example.client;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.jdiameter.api.Answer;
import org.jdiameter.api.ApplicationId;
import org.jdiameter.api.Avp;
import org.jdiameter.api.AvpDataException;
import org.jdiameter.api.AvpSet;
import org.jdiameter.api.Configuration;
import org.jdiameter.api.EventListener;
import org.jdiameter.api.IllegalDiameterStateException;
import org.jdiameter.api.InternalException;
import org.jdiameter.api.Message;
import org.jdiameter.api.MetaData;
import org.jdiameter.api.Network;
import org.jdiameter.api.NetworkReqListener;
import org.jdiameter.api.OverloadException;
import org.jdiameter.api.Request;
import org.jdiameter.api.RouteException;
import org.jdiameter.api.Session;
import org.jdiameter.api.SessionFactory;
import org.jdiameter.api.Stack;
import org.jdiameter.api.StackType;
import org.jdiameter.server.impl.StackImpl;
import org.jdiameter.server.impl.helpers.XMLConfiguration;
import org.mobicents.diameter.dictionary.AvpDictionary;
import org.mobicents.diameter.dictionary.AvpRepresentation;

public class ExampleClient implements EventListener<Request, Answer> {

	private static final Logger log = Logger.getLogger(ExampleClient.class);
	static{
			//configure logging.
			configLog4j();
	}
	
	private static void configLog4j() {
		InputStream inStreamLog4j = ExampleClient.class.getClassLoader().getResourceAsStream("log4j.properties");
		Properties propertiesLog4j = new Properties();
		try {
			propertiesLog4j.load(inStreamLog4j);
			PropertyConfigurator.configure(propertiesLog4j);
		} catch (Exception e) {
			e.printStackTrace();
		}finally
		{
			if(inStreamLog4j!=null)
			{
				try {
					inStreamLog4j.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		log.debug("log4j configured");
	}
	
	//configuration files
	private static final String configFile = "org/example/client/client-jdiameter-config.xml";
	private static final String dictionaryFile = "org/example/client/dictionary.xml";
	//our destination
	private static final String serverHost = "127.0.0.1";
	private static final String serverPort = "3868";
	private static final String serverURI = "aaa://" + serverHost + ":" + serverPort;
	//our realm
	private static final String realmName = "exchange.example.org";
	// definition of codes, IDs
	private static final int commandCode = 686;
	private static final long vendorID = 66666;
	private static final long applicationID = 33333;
	private ApplicationId authAppId = ApplicationId.createByAuthAppId(applicationID);
	private static final int exchangeTypeCode = 888;
	private static final int exchangeDataCode = 999;
	// enum values for Exchange-Type AVP
	private static final int EXCHANGE_TYPE_INITIAL = 0;
	private static final int EXCHANGE_TYPE_INTERMEDIATE = 1;
	private static final int EXCHANGE_TYPE_TERMINATING = 2;
	//list of data we want to exchange.
	private static final String[] TO_SEND = new String[] { "I want to get 3 answers", "This is second message", "Bye bye" };
	//Dictionary, for informational purposes.
	private AvpDictionary dictionary = AvpDictionary.INSTANCE;
	//stack and session factory
	private Stack stack;
	private SessionFactory factory;

	// ////////////////////////////////////////
	// Objects which will be used in action //
	// ////////////////////////////////////////
	private Session session;  // session used as handle for communication
	private int toSendIndex = 0;  //index in TO_SEND table
	private boolean finished = false;  //boolean telling if we finished our interaction

	private void initStack() {
		if (log.isInfoEnabled()) {
			log.info("Initializing Stack...");
		}
		InputStream is = null;
		try {
			//Parse dictionary, it is used for user friendly info.
			dictionary.parseDictionary(this.getClass().getClassLoader().getResourceAsStream(dictionaryFile));
			log.info("AVP Dictionary successfully parsed.");
			
			this.stack = new StackImpl();
			//Parse stack configuration
			is = this.getClass().getClassLoader().getResourceAsStream(configFile);
			Configuration config = new XMLConfiguration(is);
			factory = stack.init(config);
			if (log.isInfoEnabled()) {
				log.info("Stack Configuration successfully loaded.");
			}
			//Print info about applicatio
			Set<org.jdiameter.api.ApplicationId> appIds = stack.getMetaData().getLocalPeer().getCommonApplications();

			log.info("Diameter Stack  :: Supporting " + appIds.size() + " applications.");
			for (org.jdiameter.api.ApplicationId x : appIds) {
				log.info("Diameter Stack  :: Common :: " + x);
			}
			is.close();
			//Register network req listener, even though we wont receive requests
			//this has to be done to inform stack that we support application
			Network network = stack.unwrap(Network.class);
			network.addNetworkReqListener(new NetworkReqListener() {
				
				@Override
				public Answer processRequest(Request request) {
					//this wontbe called.
					return null;
				}
			}, this.authAppId); //passing our example app id.
			
		} catch (Exception e) {
			e.printStackTrace();
			if (this.stack != null) {
				this.stack.destroy();
			}

			if (is != null) {
				try {
					is.close();
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
			}
			return;
		}

		MetaData metaData = stack.getMetaData();
		//ignore for now.
		if (metaData.getStackType() != StackType.TYPE_SERVER || metaData.getMinorVersion() <= 0) {
			stack.destroy();
			if (log.isEnabledFor(org.apache.log4j.Level.ERROR)) {
				log.error("Incorrect driver");
			}
			return;
		}

		try {
			if (log.isInfoEnabled()) {
				log.info("Starting stack");
			}
			stack.start();
			if (log.isInfoEnabled()) {
				log.info("Stack is running.");
			}
		} catch (Exception e) {
			e.printStackTrace();
			stack.destroy();
			return;
		}
		if (log.isInfoEnabled()) {
			log.info("Stack initialization successfully completed.");
		}
	}

	/**
	 * @return
	 */
	private boolean finished() {
		return this.finished;
	}

	/**
	 * 
	 */
	private void start() {
		try {
			//wait for connection to peer
			try {
				Thread.currentThread().sleep(5000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			//do send
			this.session = this.factory.getNewSession("BadCustomSessionId;YesWeCanPassId;" + System.currentTimeMillis());
			sendNextRequest(EXCHANGE_TYPE_INITIAL);
		} catch (InternalException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalDiameterStateException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (RouteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (OverloadException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	private void sendNextRequest(int enumType) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
		Request r = this.session.createRequest(commandCode, this.authAppId, realmName, serverURI);
		// here we have all except our custom avps

		AvpSet requestAvps = r.getAvps();
		// code , value , vendor, mandatory,protected,isUnsigned32
		// (Enumerated)
		Avp exchangeType = requestAvps.addAvp(exchangeTypeCode, (long) enumType, vendorID, true, false, true); // value
																												// is
																												// set
																												// on
																												// creation
		// code , value , vendor, mandatory,protected, isOctetString
		Avp exchengeData = requestAvps.addAvp(exchangeDataCode, TO_SEND[toSendIndex++], vendorID, true, false, false); // value
																														// is
																														// set
																														// on
																														// creation
		// send
		this.session.send(r, this);
		dumpMessage(r,true); //dump info on console
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdiameter.api.EventListener#receivedSuccessMessage(org.jdiameter
	 * .api.Message, org.jdiameter.api.Message)
	 */
	@Override
	public void receivedSuccessMessage(Request request, Answer answer) {
		dumpMessage(answer,false);
		if (answer.getCommandCode() != commandCode) {
			log.error("Received bad answer: " + answer.getCommandCode());
			return;
		}
		AvpSet answerAvpSet = answer.getAvps();

		Avp exchangeTypeAvp = answerAvpSet.getAvp(exchangeTypeCode, vendorID);
		Avp exchangeDataAvp = answerAvpSet.getAvp(exchangeDataCode, vendorID);
		Avp resultAvp = answer.getResultCode();

		
		try {
			//for bad formatted request.
			if (resultAvp.getUnsigned32() == 5005 || resultAvp.getUnsigned32() == 5004) {
				// missing || bad value of avp
				this.session.release();
				this.session = null;
				log.error("Something wrong happened at server side!");
				finished = true;
			}
			switch ((int) exchangeTypeAvp.getUnsigned32()) {
			case EXCHANGE_TYPE_INITIAL:
				// JIC check;
				String data = exchangeDataAvp.getUTF8String();
				if (data.equals(TO_SEND[toSendIndex - 1])) {
					// ok :) send next;
					sendNextRequest(EXCHANGE_TYPE_INTERMEDIATE);
				} else {
					log.error("Received wrong Exchange-Data: " + data);
				}
				break;
			case EXCHANGE_TYPE_INTERMEDIATE:
				// JIC check;
				data = exchangeDataAvp.getUTF8String();
				if (data.equals(TO_SEND[toSendIndex - 1])) {
					// ok :) send next;
					sendNextRequest(EXCHANGE_TYPE_TERMINATING);
				} else {
					log.error("Received wrong Exchange-Data: " + data);
				}
				break;
			case EXCHANGE_TYPE_TERMINATING:
				data = exchangeDataAvp.getUTF8String();
				if (data.equals(TO_SEND[toSendIndex - 1])) {
					// good, we reached end of FSM.
					finished = true;
					// release session and its resources.
					this.session.release();
					this.session = null;
				} else {
					log.error("Received wrong Exchange-Data: " + data);
				}
				break;
			default:
				log.error("Bad value of Exchange-Type avp: " + exchangeTypeAvp.getUnsigned32());
				break;
			}
		} catch (AvpDataException e) {
			// thrown when interpretation of byte[] fails
			e.printStackTrace();
		} catch (InternalException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalDiameterStateException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (RouteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (OverloadException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdiameter.api.EventListener#timeoutExpired(org.jdiameter.api.
	 * Message)
	 */
	@Override
	public void timeoutExpired(Request request) {
	

	}

	private void dumpMessage(Message message, boolean sending) {
		if (log.isInfoEnabled()) {
			log.info((sending?"Sending ":"Received ") + (message.isRequest() ? "Request: " : "Answer: ") + message.getCommandCode() + "\nE2E:"
					+ message.getEndToEndIdentifier() + "\nHBH:" + message.getHopByHopIdentifier() + "\nAppID:" + message.getApplicationId());
			log.info("AVPS["+message.getAvps().size()+"]: \n");
			try {
				printAvps(message.getAvps());
			} catch (AvpDataException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	private void printAvps(AvpSet avpSet) throws AvpDataException {
		printAvpsAux(avpSet, 0);
	}

	/**
	 * Prints the AVPs present in an AvpSet with a specified 'tab' level
	 * 
	 * @param avpSet
	 *            the AvpSet containing the AVPs to be printed
	 * @param level
	 *            an int representing the number of 'tabs' to make a pretty
	 *            print
	 * @throws AvpDataException
	 */
	private void printAvpsAux(AvpSet avpSet, int level) throws AvpDataException {
		String prefix = "                      ".substring(0, level * 2);

		for (Avp avp : avpSet) {
			AvpRepresentation avpRep = AvpDictionary.INSTANCE.getAvp(avp.getCode(), avp.getVendorId());

			if (avpRep != null && avpRep.getType().equals("Grouped")) {
				log.info(prefix + "<avp name=\"" + avpRep.getName() + "\" code=\"" + avp.getCode() + "\" vendor=\"" + avp.getVendorId() + "\">");
				printAvpsAux(avp.getGrouped(), level + 1);
				log.info(prefix + "</avp>");
			} else if (avpRep != null) {
				String value = "";

				if (avpRep.getType().equals("Integer32"))
					value = String.valueOf(avp.getInteger32());
				else if (avpRep.getType().equals("Integer64") || avpRep.getType().equals("Unsigned64"))
					value = String.valueOf(avp.getInteger64());
				else if (avpRep.getType().equals("Unsigned32"))
					value = String.valueOf(avp.getUnsigned32());
				else if (avpRep.getType().equals("Float32"))
					value = String.valueOf(avp.getFloat32());
				else
					value = avp.getOctetString();

				log.info(prefix + "<avp name=\"" + avpRep.getName() + "\" code=\"" + avp.getCode() + "\" vendor=\"" + avp.getVendorId()
						+ "\" value=\"" + value + "\" />");
			}
		}
	}

	public static void main(String[] args) {
		ExampleClient ec = new ExampleClient();
		ec.initStack();
		ec.start();

		while (!ec.finished()) {
			try {
				Thread.currentThread().sleep(5000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}